
function _CheckSystemSupports(type) {
    /// algorithm see: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API
    let storage = undefined;
    try {
        storage = window[type];
        const x = '__storage_test__';
        storage.setItem(x, x);
        storage.removeItem(x);
        return true;
    }
    catch (e) {
        return e instanceof DOMException && (
            // everything except Firefox
            e.code === 22 ||
            // Firefox
            e.code === 1014 ||
            // test name field too, because code might not be present
            // everything except Firefox
            e.name === 'QuotaExceededError' ||
            // Firefox
            e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
            // acknowledge QuotaExceededError only if there's something already stored
            (storage && storage.length !== 0);
    }
}

function EraseValueFromArray(arr, value) {
    const _i = arr.indexOf(value);
    if (_i >= 0) {
        arr.splice(_i, 1);
        return true;
    }
    return false;
};

function GUID() {
    return 'S-xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

function Clamp(left, right, v) {
    if (v < left) return left;
    if (v > right) return right;
    return v;
}

// return "yyyy-mm-dd"
function Date2InputDateValue(d) {
    let _tmp = d.toJSON();
    return _tmp.slice(0, 10);
}


function DefineProperty(o, p, v, w, e, c) {
    Object.defineProperty(o, p, {value: v, writable: w, enumerable: e, configurable: c});
}

function DefineEnumProperty(o, p, v) {
    Object.defineProperty(o, p, {value: v, writable: false, enumerable: true, configurable: false});
}

function DefineFixedProperty(o, p, v) {
    Object.defineProperty(o, p, {value: v, writable: false, enumerable: false, configurable: false});
}

class Event {
    constructor() {
        this._mapListener = new Map();
    }

    Destroy(){
        this._mapListener.clear();
        this._mapListener = undefined;
    }

    RemoveListener(eventName, cbFn) {
        let _arr = this._mapListener.get(eventName);
        if (_arr) {
            let _i = _arr.indexOf(cbFn);
            if (_i >= 0) {
                _arr.splice(_i, 1);
                return true;
            }
        }
        return false;
    }

    // cbFn should bind to its caller;
    AppendListener(eventName, cbFn, priority = 0) {
        let _arr = this._mapListener.get(eventName);
        if (!_arr) {
            _arr = [];
            this._mapListener.set(eventName, _arr);
        }
        const _idx = _arr.indexOf(cbFn);
        if(_idx < 0) {
            for(let i = 0, N = _arr.length; i < N; ++i) {
                // insert as increasing order;
                if(_arr[i].priority >= priority) {
                    _arr.splice(i, 0, {priority, cbFn});
                    return true;
                }
            }
            _arr.push({priority, cbFn});
            return true;
        }
        return false;
    }

    DispatchEvent(eventName, ...rest) {
        let _arr = this._mapListener.get(eventName);
        if (_arr) {
            for(let i = _arr.length - 1; i >= 0; --i) {
                _arr[i].cbFn(rest);
            };
            return true;
        }
        return false;
    }
}

const _tempHTMLElement = document.createElement('div');
function CreateHTMLElement(content) {
    _tempHTMLElement.innerHTML = content;
    return _tempHTMLElement.firstElementChild;
}

function CreateCSSElement(content) {
    let _tmp = document.createElement('style');
    _tmp.innerHTML = content;
    return _tmp;
}

class EventDispatcher {
    constructor(event) {
        DefineFixedProperty(this, '_event', event ?? new Event());
    }
    get event() {return this._event;}

    // cbFn should bind to its caller;
    AppendListener(eventName, cbFn) {
        this._event.AppendListener(eventName, cbFn);
    }

    RemoveListener(eventName, cbFn) {
        this._event.RemoveListener(eventName, cbFn);
    }
}

class Component extends EventDispatcher {
    constructor(selector) {
        super();
        if (typeof selector == "string") {
            this._containerElem = document.querySelector(selector);
        } else if (selector instanceof HTMLElement) {
            this._containerElem = selector;
        } else {
            this._containerElem = document.body;
        }
    }
}

function QuerySelector(value, defaultElement) {
    if (typeof value == "string") {
        return document.querySelector(value);
    } else if (value instanceof HTMLElement) {
        return value;
    } else {
        return defaultElement;
    }
}

const TypeCheck = Object.freeze({
    IsArray(value) {return value && value instanceof Array;},
    IsMap(value) {return value && value instanceof Map;},
    IsPromise(value) {return value && value instanceof Promise;},
    IsInstanceOf(value, cls) {return value && value instanceof cls;},
    IsDomElement(value) {return value && value instanceof Element;},

    IsFunction(value) {return typeof value === 'function';},
    IsClass(value) {return typeof value === 'function';},
    IsString(value) {return typeof value === 'string';},
    IsNumber(value) {return typeof value === 'number';},
    IsBoolean(value) {return typeof value === 'boolean';},
    IsObject(value) {return typeof value === 'object';},
});

function ObjectEquals(x, y) {
    'use strict';

    if (x === null || x === undefined || y === null || y === undefined) {return x === y;}
    // after this just checking type of one would be enough
    if (x.constructor !== y.constructor) {return false;}
    // if they are functions, they should exactly refer to same one (because of closures)
    if (x instanceof Function) {return x === y;}
    // if they are regexps, they should exactly refer to same one (it is hard to better equality check on current ES)
    if (x instanceof RegExp) {return x === y;}
    if (x === y || x.valueOf() === y.valueOf()) {return true;}
    if (Array.isArray(x) && x.length !== y.length) {return false;}

    // if they are dates, they must had equal valueOf
    if (x instanceof Date) {return false;}

    // if they are strictly equal, they both need to be object at least
    if (!(x instanceof Object)) {return false;}
    if (!(y instanceof Object)) {return false;}

    // recursive object equality check
    var p = Object.keys(x);
    return Object.keys(y).every(function (i) {return p.indexOf(i) !== -1;}) &&
        p.every(function (i) {return objectEquals(x[i], y[i]);});
}

function DelayPromise() {
    let _resovleFn, _rejectFn;
    const _promise = new Promise((resolve, reject) => {
        _resovleFn = resolve;
        _rejectFn = reject;
    });
    return {
        Resolve: (data) => {_resovleFn(data);},
        Reject: (data) => {_rejectFn(data);},
        get promise() {return _promise;}
    }
}

function EventListenerCollection(element) {
    const _arr = [];
    return {
        Add: (type, fn) => {
            _arr.push([type, fn]);
            element.addEventListener(type, fn);
        },
        RemoveAll: () => {
            for(let [type, fn] of _arr) {
                element.removeEventListener(type, fn);
            }
            _arr.length = 0;
        }

    }
}

function F(obj){return Object.freeze(obj);}

const _EMPTY_FN_ = () => {}

let Utils = Object.freeze(
    {
        QuerySelector,
        Clamp,
        GUID,
        EraseValueFromArray,
        Date2InputDateValue,
        DefineProperty,
        DefineFixedProperty,
        DefineEnumProperty,
        CreateHTMLElement,
        CreateCSSElement,
        ObjectEquals,
        F,
    });

export {
    Component,
    Event,
    EventDispatcher,
    Utils,
    TypeCheck,
    DelayPromise,
    EventListenerCollection,
    _EMPTY_FN_,
}

